Skip to content

[security] /v1/* 与 /api/admin/* 默认 fail-closed;新增首启自助初始化与全局未配置 Key 警告#109

Merged
james-6-23 merged 2 commits into
james-6-23:mainfrom
ImogeneOctaviap794:security/fail-closed-and-bootstrap
Apr 30, 2026
Merged

[security] /v1/* 与 /api/admin/* 默认 fail-closed;新增首启自助初始化与全局未配置 Key 警告#109
james-6-23 merged 2 commits into
james-6-23:mainfrom
ImogeneOctaviap794:security/fail-closed-and-bootstrap

Conversation

@ImogeneOctaviap794
Copy link
Copy Markdown
Contributor

@ImogeneOctaviap794 ImogeneOctaviap794 commented Apr 30, 2026

漏洞概要 / TL;DR

当前 main 分支(含已发布的 v2.0.0)默认存在两条未授权直通通道:

  1. /v1/* 在未创建任何对外 API Key 时不做鉴权:任何能访问端口的人都可以
    直接调用 /v1/chat/completions/v1/responses/v1/messages
    /v1/images/generations 等接口,消耗账号池中所有 Codex 账号的配额
  2. /api/admin/* 在未设置 ADMIN_SECRET 时同样无鉴权放行:攻击者可以
    读取/写入账号、删除 API Key、改设置、触发 OAuth 流程等。

我们目前已统计到至少 36 个公网部署站点 受此默认行为影响,
攻击者无需任何 key 即可直接通过这些站点的 /v1/* 端点消耗账号池配额。
出于负责任披露的考虑,本 PR 不附带具体站点列表。建议维护者合并后
通过 release notes 与现有部署用户沟通升级。

相关代码(修复前):

  • proxy/handler.go: authMiddlewareif !h.hasAnyKeys() { c.Next(); return } —— fail-open
  • admin/handler.go: adminAuthMiddlewareif adminSecret == "" { c.Next(); return } —— fail-open

影响范围

项目 默认风险
受影响版本 至少包含 v2.0.0 及之前所有发布;逻辑长期存在
受影响接口 /v1/* 全部、/api/admin/* 全部
攻击难度 极低(任意 HTTP 客户端,无需鉴权)
危害 账号池配额被白嫖、账号被删除/导出、Refresh Token 暴露
已知公网受影响站点 ≥ 36 个

修复方案(fail-closed + first-run bootstrap)

1. /v1/* —— proxy/handler.go

  • 默认行为改为 fail-closed:未配置任何 API Key 时返回 503 service_unavailable
    并写入安全审计日志 V1_BLOCKED_NO_KEYS
  • 新增显式 opt-out:CODEX_ALLOW_ANONYMOUS=true 仅供内网 / 测试场景,
    打开后启动 banner 会持续告警。

2. /api/admin/* —— admin/handler.go

  • 默认 fail-closed,未配置 ADMIN_SECRET 时返回 503 + code: bootstrap_required
    并写入审计日志 ADMIN_BLOCKED_NO_SECRET
  • 取消之前提交里"启动时自动随机生成 ADMIN_SECRET 并写入数据库"的快捷做法 ——
    避免出现"用户不知道但其实已存在"的默认密钥(这种自动密钥本身也是攻击面)。

3. 首启自助初始化 —— admin/bootstrap.go(新增)

两个无鉴权端点(注册在 admin 鉴权中间件之外):

  • GET /api/admin/bootstrap-status{needs_bootstrap, source: env|database|empty}
  • POST /api/admin/bootstrap → 仅当系统未初始化时接受一次写入,约束包含:
    • sync.Mutex + 进入临界区的 二次检查(杜绝并发写入)
    • 全局 限频 60s / 20 次(防扫描穷举)
    • 最小 8 位长度校验
    • 已初始化后所有调用一律 409
    • 全程 SecurityAuditLog

4. 前端 —— frontend/src/components/AuthGate.tsxSecurityBanner.tsx(新增)

  • AuthGate 启动时先调 bootstrap-status
    • needs_bootstrap=true → 显示「首次使用:设置管理密钥」表单(含二次输入校验、最少 8 位)
    • 提交成功后直接落入 Dashboard,无需再走一次登录页
  • 新增全局 SecurityBanner:登录后周期性查询 API Key 数量,为 0 时显示
    红色横幅提示「尚未配置对外 API Key」并提供跳转入口;用户可以「我知道了」
    暂时关闭 24h。

5. 监听地址 / 文档

  • 默认绑定继续保持 0.0.0.0 以兼容 Docker ports:、反向代理与生产部署 ——
    曾在迭代中尝试默认 127.0.0.1,但会破坏所有 docker-compose 部署,
    最终回退为:
    • 默认 0.0.0.0 + 启动 banner 提示需配防火墙 + HTTPS
    • 提供 CODEX_BIND=127.0.0.1 给希望严格回环的用户
  • .env.example / .env.sqlite.example 增补 CODEX_BINDCODEX_ALLOW_ANONYMOUSADMIN_SECRET 三个开关的说明与最佳实践

行为变化(破坏性,请阅读)

场景 升级前 升级后
首次部署、未在 .env 设置 ADMIN_SECRET 直接进 Dashboard,无任何鉴权 浏览器引导用户在「首次初始化」页面设置一次
部署完毕但忘了创建 API Key /v1/* 仍可被任意调用消耗配额 /v1/* 返回 503,提示需创建 Key
期望保留旧"无鉴权直通"行为 —— 显式设置 CODEX_ALLOW_ANONYMOUS=true 后启动(banner 会持续告警)
Docker / docker-compose 部署 默认监听 0.0.0.0,端口映射可用 不变(默认仍是 0.0.0.0

升级路径:

# 已部署的实例:
1. 拉取新镜像 / 二进制并重启
2. 浏览器访问 /admin/,按提示设置一个强随机 ADMIN_SECRET(≥8 位)
3. 在「API 密钥」页面创建至少一把对外 Key 后再恢复对外提供服务

测试矩阵(手工验证全部通过)

# 场景 期望 结果
1 未初始化 GET /api/admin/bootstrap-status needs_bootstrap:true
2 未初始化访问其他 admin 端点 503 + code: bootstrap_required
3 bootstrap 写入弱密钥(<8 位) 400 拒绝
4 bootstrap 写入合法密钥 200 ok:true
5 已初始化后再调用 bootstrap 409 拒绝
6 未配置 API Key 调 /v1/models 503 service_unavailable
7 配置 CODEX_ALLOW_ANONYMOUS=true 后调 /v1/models 放行 + 启动 banner 告警
8 用初始化时设置的密钥访问 /api/admin/accounts 200
9 Docker 默认监听 0.0.0.0 端口映射 正常
10 CODEX_BIND=127.0.0.1 时局域网 IP 访问 Connection refused + lsof 显示 127.0.0.1:8081

文件清单

新增:

  • admin/bootstrap.go
  • frontend/src/components/SecurityBanner.tsx

修改:

  • proxy/handler.go/v1/* fail-closed
  • admin/handler.go/api/admin/* fail-closed + 注册 bootstrap 路由
  • config/config.go — 新增 BindAddressAllowAnonymousV1
  • main.go — 启动安全自检 banner(不再自动生成密钥)
  • frontend/src/components/AuthGate.tsx — 新增 need_bootstrap 状态与初始化页
  • frontend/src/components/Layout.tsx — 注入全局 SecurityBanner
  • .env.example.env.sqlite.example — 文档化三个安全相关开关

致维护者

强烈建议:

  1. 尽快合并并发布带补丁的 patch release(例如 v2.0.1),并在 release notes
    中明确列出「安全修复 + 行为变化」。
  2. 通过 GitHub Security Advisory 私下通知已知部署用户(如有 telemetry / Discord)。
  3. 考虑同步发布一个 docs/SECURITY.md,告知未来漏洞披露渠道。

Summary by CodeRabbit

  • New Features

    • First-time bootstrap flow for admin secret setup.
    • Toggleable anonymous access for /v1/* endpoints.
    • UI security banner showing API key status.
    • Configurable server bind address (CODEX_BIND).
  • Behavior Changes

    • Admin/API endpoints now return 503 when service is uninitialized or intentionally locked down.
  • Documentation

    • Updated .env examples with bootstrap, bind, and anonymous-access guidance.

…n bootstrap

Previously the proxy fell back to fail-open behavior when no API key /
ADMIN_SECRET was configured: any unauthenticated client could hit /v1/*
and consume the entire account pool quota, or call /api/admin/* and
read / mutate sensitive data. At least 36 publicly deployed instances
have been observed to be vulnerable in their default configuration.

This commit changes the defaults:

- /v1/*: refuse with 503 when no public API key has been created.
  An explicit opt-out (CODEX_ALLOW_ANONYMOUS=true) is provided for
  internal / testing scenarios; the startup banner keeps warning while
  it is active.
- /api/admin/*: refuse with 503 + code "bootstrap_required" when
  ADMIN_SECRET is unset.
- New first-run bootstrap (admin/bootstrap.go):
    GET  /api/admin/bootstrap-status
    POST /api/admin/bootstrap   (only when uninitialized)
  Hardening: sync.Mutex + double-check, fixed-window rate limit
  (60s / 20 req), minimum 8-char secret, full SecurityAuditLog.
- Drop the previous "auto-generate ADMIN_SECRET" shortcut so users
  always know which secret is in use.
- Default bind stays 0.0.0.0 to keep Docker / reverse-proxy deployments
  working; CODEX_BIND lets users restrict to loopback.
- Frontend AuthGate gains a "First-run setup" screen with a confirm
  field; a global SecurityBanner warns logged-in admins when no public
  API key has been created.
- .env.example / .env.sqlite.example document the new switches.

Behavior change: existing deployments must (a) set an ADMIN_SECRET via
the new browser bootstrap page on first restart and (b) create at least
one public API key before /v1/* will start serving traffic again. Users
who consciously want the legacy unauthenticated behavior must opt in
with CODEX_ALLOW_ANONYMOUS=true.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Adds a first-run admin bootstrap flow with two unauthenticated endpoints, enforces fail-closed admin/auth behavior, introduces bind-address and anonymous-v1 config, surfaces a startup security banner, updates proxy auth to block v1 when no keys exist unless allowed, and adds frontend bootstrap UI and security banner.

Changes

Cohort / File(s) Summary
Configuration docs
\.env\.example, \.env\.sqlite\.example
Document CODEX_BIND (server bind address) and CODEX_ALLOW_ANONYMOUS (anonymous /v1/* toggle); expand ADMIN_SECRET initialization notes and remove prior API-keys auth doc.
Config model
config/config.go
Add BindAddress string and AllowAnonymousV1 bool; load from CODEX_BIND (default 0.0.0.0) and CODEX_ALLOW_ANONYMOUS.
Admin bootstrap
admin/bootstrap.go, admin/handler.go
Add GET /api/admin/bootstrap-status and POST /api/admin/bootstrap; fixed-window rate limiter, input validation, mutex-protected double-check-and-write of system settings, audit logging, and 503 behavior when ADMIN_SECRET env absent.
Proxy auth changes
proxy/handler.go
Change auth middleware to deny-by-default when no API keys exist: proceed only if AllowAnonymousV1 true; otherwise log security audit and return 503.
Startup & runtime logging
main.go
Print a security-state banner at startup (AdminSecret state, API key count, anonymous flag, bind publicity); use configured bind address when listening; display localhost for 0.0.0.0/::.
Frontend - auth & bootstrap UI
frontend/src/components/AuthGate.tsx
Add need_bootstrap state and bootstrap form flow; query /api/admin/bootstrap-status; validate and POST bootstrap secret; on success set admin key and transition to authenticated; adjust some UI copy usage.
Frontend - security banner & layout
frontend/src/components/SecurityBanner.tsx, frontend/src/components/Layout.tsx
Add SecurityBanner component (polls API key count, dismissible, shows warning when zero keys) and mount it in Layout before children.

Sequence Diagrams

sequenceDiagram
    participant User as Browser User
    participant Frontend as Frontend (AuthGate)
    participant AdminAPI as Admin API (Bootstrap)
    participant DB as Database

    User->>Frontend: Load SPA
    Frontend->>AdminAPI: GET /api/admin/bootstrap-status
    AdminAPI->>DB: GetSystemSettings()
    DB-->>AdminAPI: No AdminSecret
    AdminAPI-->>Frontend: { needs_bootstrap: true }
    Frontend->>User: Render bootstrap form

    User->>Frontend: Submit admin secret
    Frontend->>Frontend: Validate secret
    Frontend->>AdminAPI: POST /api/admin/bootstrap {admin_secret}
    AdminAPI->>AdminAPI: Rate-limit check & env check
    AdminAPI->>DB: GetSystemSettings() (double-check)
    DB-->>AdminAPI: No record
    AdminAPI->>DB: UpdateSystemSettings(AdminSecret)
    DB-->>AdminAPI: Success
    AdminAPI-->>Frontend: 200 OK
    Frontend->>Frontend: Persist admin key, set authenticated
    Frontend->>User: Redirect to app
Loading
sequenceDiagram
    participant Client as API Client
    participant Proxy as Proxy (authMiddleware)
    participant Config as Config (AllowAnonymousV1)
    participant DB as Database (API keys)

    Client->>Proxy: Request /v1/...
    Proxy->>DB: hasAnyKeys()?
    DB-->>Proxy: No keys
    Proxy->>Config: Read AllowAnonymousV1
    alt AllowAnonymousV1 = true
        Proxy->>Client: Proceed (forward request)
    else AllowAnonymousV1 = false
        Proxy->>Proxy: Log audit V1_BLOCKED_NO_KEYS
        Proxy-->>Client: 503 Service Unavailable (not configured)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A nimble hop for first-run cheer,
I tuck a secret, spring it near;
Banners warn where keys are none,
Bind the host and gates undone;
Now setup hums — the rabbit’s done.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main security changes: /v1/* and /api/admin/* endpoints now default to fail-closed, plus new first-run bootstrap initialization and API key misconfiguration warning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Review rate limit: 1/3 review remaining, refill in 25 minutes and 47 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/AuthGate.tsx (1)

79-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed on health-check failures instead of authenticating by default.

checkAuth currently treats unexpected statuses and fetch errors as authenticated (Line 86, Line 89), which is a fail-open path and conflicts with this PR’s security intent.

Suggested patch
-      if (res.status === 401) {
+      if (res.status === 401) {
         setAdminKey('')
         setStatus('need_login')
       } else if (res.status === 503) {
         setStatus('need_bootstrap')
-      } else {
+      } else if (res.ok) {
         setStatus('authenticated')
+      } else {
+        setStatus('need_login')
       }
     } catch {
-      setStatus('authenticated')
+      setStatus('need_login')
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AuthGate.tsx` around lines 79 - 90, The health-check
logic in checkAuth incorrectly treats non-401/503 responses and fetch errors as
authenticated; change it to fail closed by only marking authenticated when the
response is explicitly successful (e.g., res.status === 200), and for any other
status or any caught exception call setStatus('need_login') (and clear
credentials as appropriate with setAdminKey('')). Update the branch that
currently sets status in the else and the catch block to enforce this strict
check using the response status and exceptions, referencing the checkAuth
function and the setStatus/setAdminKey calls to locate and modify the code.
🧹 Nitpick comments (1)
admin/bootstrap.go (1)

113-131: ⚡ Quick win

Add audit logs for 400 validation rejections on bootstrap endpoint.

ShouldBindJSON/empty/too-short/too-long branches return 400 without SecurityAuditLog, so unauthenticated probing attempts are not fully visible in security telemetry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/bootstrap.go` around lines 113 - 131, In the bootstrap handler in
admin/bootstrap.go, add SecurityAuditLog calls before each 400 response in the
ShouldBindJSON, empty secret, too-short (utf8.RuneCountInString(secret) <
bootstrapMinSecret), and too-long (len(secret) > bootstrapMaxSecret) branches so
validation rejections are recorded; include context such as the request remote
IP (from c.Request.RemoteAddr or c.ClientIP()), the attempted AdminSecret masked
or its length, the specific validation error string used in the c.JSON response,
and the handler name (e.g., the current bootstrap handler) when invoking
SecurityAuditLog; ensure you call the same SecurityAuditLog API used elsewhere
in the codebase and keep behavior otherwise unchanged (still return the same 400
JSON responses afterward).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 13-16: Update the ADMIN_SECRET description in .env.example to
reflect the actual bootstrap behavior: remove the "首次启动自动生成随机密钥" wording and
state that if ADMIN_SECRET is not set the server will be in fail-closed mode and
the first-time initialization must be completed via the /admin endpoint;
reference the bootstrap/fail-closed behavior so maintainers can correlate the
doc string with the initialization flow (look for ADMIN_SECRET and
bootstrap/fail-closed).

In @.env.sqlite.example:
- Around line 13-16: Update the ADMIN_SECRET comment in .env.sqlite.example to
reflect the current bootstrap behavior: replace the line that says the secret is
auto-generated on first start and written to DB with text stating that if
ADMIN_SECRET is not set the app will fail-closed during bootstrap and that the
admin account must be initialized via the /admin endpoint or by explicitly
setting the ADMIN_SECRET env var; ensure the comment references the ADMIN_SECRET
variable name so readers can find it easily.

In `@admin/bootstrap.go`:
- Around line 35-43: The rollover is race-prone: update bootstrapAllowRate to
perform a CAS-based window rollover on bootstrapState.windowStart (compare with
the loaded winStart and set to now using CompareAndSwap) so only one goroutine
resets the window; when the CAS succeeds reset bootstrapState.count to 0, and
otherwise proceed without resetting. After ensuring the correct windowStart,
atomically increment bootstrapState.count and compare against bootstrapMaxPerWin
(retry the increment if necessary or use the atomic Add result) to decide
allowance; reference bootstrapAllowRate, bootstrapState.windowStart,
bootstrapState.count, bootstrapWindowSec, and bootstrapMaxPerWin.

In `@config/config.go`:
- Line 73: 更新配置注释以与实际行为一致:在 config.go 中修正 BindAddress 的注释(以及同一文件中第 104-106
行相关注释)以反映当前默认值和行为(默认 0.0.0.0 且 ADMIN_SECRET 由 bootstrap 管理),确保提到需要显式设置为
127.0.0.1 或 0.0.0.0 时的含义和 bootstrap 管理自动 ADMIN_SECRET
的事实,并在注释中提供明确的安全提醒,便于未来维护者正确理解 BindAddress 与自动密钥生成的关系(定位标识符:BindAddress)。

In `@frontend/src/components/AuthGate.tsx`:
- Around line 147-173: Add a client-side max-length check in handleBootstrap to
mirror the backend validation: after trimming bsSecret and bsConfirm and before
setBsSubmitting(true), check if secret.length exceeds the backend max (use the
same MAX_SECRET_LEN value or import/define a matching constant) and call
setBsError(copy.errTooLong) then return; reference the handleBootstrap function,
bsSecret/bsConfirm, MIN_SECRET_LEN, setBsError and copy to locate where to
insert the check.

In `@frontend/src/components/SecurityBanner.tsx`:
- Around line 12-19: The banner text in SecurityBanner (the i18n key en.body and
corresponding body in Chinese) is absolute and incorrect when
CODEX_ALLOW_ANONYMOUS=true; update the copy to indicate the default behavior or
call out the anonymous-mode exception (e.g., prepend "By default," or append
"unless anonymous mode is enabled") so the message is accurate for both modes;
modify the strings referenced by body in the locale objects and ensure the CTA
and dismiss keys remain unchanged.

---

Outside diff comments:
In `@frontend/src/components/AuthGate.tsx`:
- Around line 79-90: The health-check logic in checkAuth incorrectly treats
non-401/503 responses and fetch errors as authenticated; change it to fail
closed by only marking authenticated when the response is explicitly successful
(e.g., res.status === 200), and for any other status or any caught exception
call setStatus('need_login') (and clear credentials as appropriate with
setAdminKey('')). Update the branch that currently sets status in the else and
the catch block to enforce this strict check using the response status and
exceptions, referencing the checkAuth function and the setStatus/setAdminKey
calls to locate and modify the code.

---

Nitpick comments:
In `@admin/bootstrap.go`:
- Around line 113-131: In the bootstrap handler in admin/bootstrap.go, add
SecurityAuditLog calls before each 400 response in the ShouldBindJSON, empty
secret, too-short (utf8.RuneCountInString(secret) < bootstrapMinSecret), and
too-long (len(secret) > bootstrapMaxSecret) branches so validation rejections
are recorded; include context such as the request remote IP (from
c.Request.RemoteAddr or c.ClientIP()), the attempted AdminSecret masked or its
length, the specific validation error string used in the c.JSON response, and
the handler name (e.g., the current bootstrap handler) when invoking
SecurityAuditLog; ensure you call the same SecurityAuditLog API used elsewhere
in the codebase and keep behavior otherwise unchanged (still return the same 400
JSON responses afterward).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b80473aa-b206-4727-8ed4-8efa780f7a5e

📥 Commits

Reviewing files that changed from the base of the PR and between ed6d300 and e67b0fc.

📒 Files selected for processing (10)
  • .env.example
  • .env.sqlite.example
  • admin/bootstrap.go
  • admin/handler.go
  • config/config.go
  • frontend/src/components/AuthGate.tsx
  • frontend/src/components/Layout.tsx
  • frontend/src/components/SecurityBanner.tsx
  • main.go
  • proxy/handler.go

Comment thread .env.example
Comment thread .env.sqlite.example
Comment thread admin/bootstrap.go
Comment thread config/config.go Outdated
Comment thread frontend/src/components/AuthGate.tsx
Comment thread frontend/src/components/SecurityBanner.tsx Outdated
- config/config.go: align comments with current behaviour (default bind 0.0.0.0,
  security guarded by fail-closed + browser bootstrap, no auto-generated secret)
- .env.example, .env.sqlite.example: rewrite ADMIN_SECRET notes to reflect
  the browser-based first-run bootstrap flow
- admin/bootstrap.go: harden bootstrapAllowRate() with CAS-based window
  rollover so concurrent goroutines cannot reset the counter twice and
  exceed bootstrapMaxPerWin
- frontend AuthGate: mirror backend max-length check (256 chars) to fail
  fast in the UI before sending the request
- frontend SecurityBanner: soften absolute wording, mention CODEX_ALLOW_ANONYMOUS
  exception so the copy is consistent with the actual server behaviour
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/components/AuthGate.tsx (1)

126-148: 💤 Low value

Inconsistent i18n: mixed t() and copy usage in login flow.

The login handler and UI mix i18n approaches:

  • Line 128 uses t('auth.error') while lines 138/144 use copy.loginError
  • Line 272 uses t('settings.adminSecret') while line 278 uses copy.loginPlaceholder

For consistency (and to ensure the hardcoded COPY strings are actually used), consider replacing remaining t() calls with copy.* equivalents.

♻️ Suggested fix
   const handleLogin = async () => {
     if (!inputKey.trim()) {
-      setError(t('auth.error'))
+      setError(copy.loginError)
       return
     }

For line 272, either add a secretLabel key to COPY or keep using t() if this label should remain in the i18n system.

Also applies to: 272-272

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AuthGate.tsx` around lines 126 - 148, The code mixes
i18n methods (t()) and the COPY object; update the login flow for consistency by
replacing t('auth.error') in handleLogin with the appropriate COPY key (e.g.,
copy.loginError), and ensure the settings label t('settings.adminSecret') and
the input placeholder use COPY (e.g., copy.secretLabel and
copy.loginPlaceholder); if those COPY keys do not exist, add them to the COPY
object and then reference copy.secretLabel/copy.loginPlaceholder in the
component, keeping all uses in handleLogin (setError, catch block), setAdminKey,
setStatus, inputKey, and UI labels consistent with COPY.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@admin/bootstrap.go`:
- Around line 98-106: The doc comment above PostBootstrap currently says the
minimum secret length is 12 runes but the code defines bootstrapMinSecret = 8;
update the comment to reflect the actual constraint (8 runes) so the
documentation matches the constant, e.g. change "校验最小长度(12 个 rune)" to "校验最小长度(8
个 rune)"; verify references to PostBootstrap and bootstrapMinSecret in the
comment are consistent after the edit.

---

Nitpick comments:
In `@frontend/src/components/AuthGate.tsx`:
- Around line 126-148: The code mixes i18n methods (t()) and the COPY object;
update the login flow for consistency by replacing t('auth.error') in
handleLogin with the appropriate COPY key (e.g., copy.loginError), and ensure
the settings label t('settings.adminSecret') and the input placeholder use COPY
(e.g., copy.secretLabel and copy.loginPlaceholder); if those COPY keys do not
exist, add them to the COPY object and then reference
copy.secretLabel/copy.loginPlaceholder in the component, keeping all uses in
handleLogin (setError, catch block), setAdminKey, setStatus, inputKey, and UI
labels consistent with COPY.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: daac5f8b-666a-418d-9770-5463e70aa662

📥 Commits

Reviewing files that changed from the base of the PR and between e67b0fc and 4d0b51f.

📒 Files selected for processing (6)
  • .env.example
  • .env.sqlite.example
  • admin/bootstrap.go
  • config/config.go
  • frontend/src/components/AuthGate.tsx
  • frontend/src/components/SecurityBanner.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • .env.sqlite.example
  • .env.example

Comment thread admin/bootstrap.go
Comment on lines +98 to +106
// PostBootstrap 接收用户在浏览器中输入的初始管理密钥并写入数据库。
//
// 安全约束:
// 1. 仅在系统未配置 ADMIN_SECRET 时可用,否则一律 409;
// 2. 通过互斥锁 + 双重检查避免并发写入;
// 3. 简单全局限频,防止扫描器穷举;
// 4. 校验最小长度(12 个 rune),避免过弱密钥;
// 5. 全程审计日志。
func (h *Handler) PostBootstrap(c *gin.Context) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Comment says 12 runes but constant is 8.

The doc comment on line 104 states "校验最小长度(12 个 rune)" but bootstrapMinSecret = 8 on line 31. Update the comment to match the actual constraint.

📝 Suggested fix
 // 安全约束:
 //  1. 仅在系统未配置 ADMIN_SECRET 时可用,否则一律 409;
 //  2. 通过互斥锁 + 双重检查避免并发写入;
 //  3. 简单全局限频,防止扫描器穷举;
-//  4. 校验最小长度(12 个 rune),避免过弱密钥;
+//  4. 校验最小长度(8 个 rune),避免过弱密钥;
 //  5. 全程审计日志。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/bootstrap.go` around lines 98 - 106, The doc comment above
PostBootstrap currently says the minimum secret length is 12 runes but the code
defines bootstrapMinSecret = 8; update the comment to reflect the actual
constraint (8 runes) so the documentation matches the constant, e.g. change
"校验最小长度(12 个 rune)" to "校验最小长度(8 个 rune)"; verify references to PostBootstrap
and bootstrapMinSecret in the comment are consistent after the edit.

@james-6-23 james-6-23 merged commit 95ef15b into james-6-23:main Apr 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants